/*
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  Author: g0tsu
 *  Email:  g0tsu at dnmx.0rg
 */

#define FUSE_USE_VERSION 31
#include <config.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fuse.h>
#include <fuse_opt.h>
#include <fuse_lowlevel.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <pthread.h>
#include <zip.h>
#include <zipfs.h>
#include <stddef.h>
#include <pthread.h>

struct file_handle {
  unsigned long fd;
  char *tmpfilename;
};

struct zipfs zipfs;
static void usage(const char* progname);
static int zipfs_create(const char *path, mode_t mode,
    struct fuse_file_info *fi);

static void zipfs_entries_reload(uint8_t force) {
  time_t cur = time(NULL);

  if (force == 0 && (cur - zipfs.last_update) < ZIPFS_ENTRIES_UPDATE_TIMEOUT)
    return;

  zipfs.last_update = cur;
  zipfs_free_entries();
  zipfs_fill_entries();
}

#define ZIPFS_TMP_TEMPLATE "zipfsXXXXXXX"
static char *zipfs_create_tmpfile(void) {
  const int TMPBSIZ = BUFSIZ + 32;
  char buf[TMPBSIZ];
  int tfd;

  memset(&buf, 0, TMPBSIZ);
  snprintf(buf, TMPBSIZ, "%s/%s", zipfs.tmpdir, ZIPFS_TMP_TEMPLATE);

  if ((tfd = mkstemp(buf)) == -1)
    return NULL;

  close(tfd);
  return strdup(buf);
}

static char *zipfs_trim_path(const char *str) {
  int len = strlen(str);
  char *path = strdup(str);

  if (len > 0 && path[0] == '/') {
    memcpy(path, str + 1, len - 1);
    path[len-1] = 0;
  }

  return path;
}

static int zipfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
    off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
  (void) offset;
  (void) fi;
  (void) flags;
  (void) offset;
  (void) fi;
  (void) flags;

  struct zint_arr *idxs;
  struct stat st;

  pthread_mutex_lock(&zipfs.lock);
  zipfs_entries_reload(0);
  pthread_mutex_unlock(&zipfs.lock);

  filler(buf, ".", NULL, 0, 0);
  filler(buf, "..", NULL, 0, 0);

  idxs = zipfs_find_entries(path, strlen(path));

  for (int i = 0; i < idxs->size; i++) {
    int idx = *idxs->arr[i];
    memset(&st, 0, sizeof(st));

    st.st_mode = S_IFREG | 0644;
    st.st_nlink = 1;
    st.st_size = zipfs.entries[idx]->size;

    if (zipfs.entries[idx]->isdir == 1) {
      st.st_mode = S_IFDIR | 0755;
      st.st_nlink = 2;
    }

    filler(buf, zipfs.entries[idx]->name, &st, 0 ,0);
  }

  zipfs_free_zint_arr(idxs);
  return 0;
}

static int zipfs_getattr(
    const char *path, struct stat *stbuf, struct fuse_file_info *fi) {
  (void) fi;

  int res = -ENOENT;
  char *tpath = zipfs_trim_path(path);

  pthread_mutex_lock(&zipfs.lock);
  zipfs_entries_reload(0);
  pthread_mutex_unlock(&zipfs.lock);

  memset(stbuf, 0, sizeof(struct stat));
  stbuf->st_atime = stbuf->st_mtime = time(NULL);
  stbuf->st_uid = getuid();
  stbuf->st_gid = getgid();

  if (strcmp(path, "/") == 0) {
    stbuf->st_mode = S_IFDIR | 0755;
    stbuf->st_nlink = 2;
    res = 0;
  } else if (zipfs.entries_size == 0) {
      res = -ENOENT;
  } else {
    for (int i = 0; i < zipfs.entries_size; i++) {
      if (strcmp(tpath, zipfs.entries[i]->orig) == 0 ) {

        stbuf->st_size = zipfs.entries[i]->size;

        if (zipfs.entries[i]->isdir == 1) {
          stbuf->st_mode = S_IFDIR | 0755;
          stbuf->st_nlink = 2;
        } else {
          stbuf->st_mode = S_IFREG | 0644;
          stbuf->st_nlink = 1;
        }

        res = 0;
        break;
      }
    }
  }

  free(tpath);
  return res;
}

static void *zipfs_init(struct fuse_conn_info *conn, struct fuse_config *cfg) {
  (void) conn;
  (void) cfg;
  zipfs_entries_reload(1);
  return NULL;
}

static int zipfs_read(const char *path, char *buf,
    size_t size, off_t offset, struct fuse_file_info *fi) {
  (void) path;
  (void) fi;
  (void) offset;

  struct file_handle *cfi = (struct file_handle *)(intptr_t)fi->fh;

  if (!cfi->fd)
    return -ENOENT;

  return pread(cfi->fd, buf, size, offset);
}

static int zipfs_unlink_multi(const char *path) {
  char *tpath;
  char **delt;
  int res = 0;
  struct zip_t *zip;

  if(strlen(path) == 1 && strcmp(path, "/") != 0)
    return -ENOENT;

  zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'd');

  if (!zip) {
    fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
    return -ENOENT;
  }

  tpath = zipfs_trim_path(path);
  delt = malloc(sizeof(char) * 2);
  delt[0] = tpath;

  if (zip_entries_delete(zip, delt, 1) < 0) {
    fprintf(stderr, "Unable to open entry: %s\n", tpath);
    res = -ENOENT;
  }

  free(delt);
  free(tpath);
  zip_close(zip);
  zipfs_entries_reload(1);

  return res;
}

static int zipfs_unlink(const char *path) {
  int res;
  pthread_mutex_lock(&zipfs.lock);
  res = zipfs_unlink_multi(path);
  pthread_mutex_unlock(&zipfs.lock);
  return res;
}

static int zipfs_open_append(const char *path, struct fuse_file_info *fi) {
  char *tpath;
  int res = 0;

  pthread_mutex_lock(&zipfs.lock);
  struct zip_t *zip = zip_open(zipfs.zipfile, 0, 'r');
  struct file_handle *cfi = malloc(sizeof(*cfi));

  cfi->tmpfilename = zipfs_create_tmpfile();

  if (!cfi->tmpfilename) {
    fprintf(stderr, "Unable to create tempfile\n");
    res = -ENOENT;
    goto on_clean;
  }

  if (!zip) {
    fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
    res = -ENOENT;
    goto on_clean;
  }

  tpath = zipfs_trim_path(path);

  if (zip_entry_open(zip, tpath) < 0) {
    fprintf(stderr, "Unable to open entry: %s\n", tpath);
    free(tpath);
    res = -ENOENT;
    goto on_clean;
  }

  free(tpath);

  if (zip_entry_fread(zip, cfi->tmpfilename) < 0) {
    fprintf(stderr, "Unable to extract entry: %s\n", cfi->tmpfilename);
    res =  -ENOENT;
    goto on_clean;
  }

  cfi->fd = open(cfi->tmpfilename, O_RDONLY);
  fi->fh = (intptr_t)cfi;

on_clean:
  zip_entry_close(zip);
  zip_close(zip);
  pthread_mutex_unlock(&zipfs.lock);
  return res;
}

static int zipfs_open(const char *path, struct fuse_file_info *fi) {
  struct file_handle *cfi;
  char *tmpfilename;
  fi->fh = 0;

  if (fi->flags & O_APPEND || fi->flags == 0x20201) {
    printf("zipfs_open append\n");
    cfi = malloc(sizeof(*cfi));
    cfi->tmpfilename = NULL;
    tmpfilename = zipfs_create_tmpfile();
    cfi->fd = open(tmpfilename, O_WRONLY | O_CREAT | O_TRUNC, 0600);
    fi->fh = (intptr_t)cfi;
    free(tmpfilename);
    return 0;
  } else {
    return zipfs_open_append(path, fi);
  }

  return 0;
}

static int zipfs_release(const char *path, struct fuse_file_info *fi) {
  (void) path;
  (void) fi;
  struct zip_t *zip;
  char *tpath;
  int res = 0;
  struct file_handle *cfi;

  if (!fi->fh)
    return -ENOENT;

  cfi = (struct file_handle *)(intptr_t)fi->fh;

  if (cfi->fd) {
    close(cfi->fd);
  }

  pthread_mutex_lock(&zipfs.lock);

  if (cfi->tmpfilename) {
    zipfs_unlink_multi(path);
    zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'a');

    if (!zip) {
      fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
      res = -ENOENT;
      goto on_clean;
    }

    tpath = zipfs_trim_path(path);

    if (zip_entry_open(zip, tpath) < 0) {
      fprintf(stderr, "Unable to open entry: %s\n", tpath);
      res = -ENOENT;
      goto on_clean;
    }

    if (zip_entry_fwrite(zip, cfi->tmpfilename) != 0) {
      fprintf(stderr, "Unable to zip_entry_fwrite: %s [%s]\n", tpath, cfi->tmpfilename);
      res = -ENOENT;
    }

    free(tpath);
    zip_entry_close(zip);
    zip_close(zip);
  }


on_clean:
  if (cfi->tmpfilename) {
    unlink(cfi->tmpfilename);
    free(cfi->tmpfilename);
  }

  free(cfi);
  zipfs_entries_reload(1);
  pthread_mutex_unlock(&zipfs.lock);
  return res;
}

static int zipfs_write(const char *path, const char *buf, size_t size,
    off_t offset, struct fuse_file_info *fi) {
  (void) path;
  (void) buf;
  (void) offset;
  (void) fi;
  struct file_handle *cfi = (struct file_handle *)(intptr_t)fi->fh;

  if (!cfi || !cfi->fd)
    return -ENOENT;

  return pwrite(cfi->fd, buf, size, offset);
}

static int zipfs_rename(const char *from, const char *to, unsigned int flags) {
  (void) flags;
  int res = 0;
  char *tpath = NULL,
       *tmpfile = NULL;
  struct zip_t *zip;
  pthread_mutex_lock(&zipfs.lock);

  zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'r');

  if (!zip) {
    fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
    res = -ENOENT;
    goto on_clean;
  }

  tpath = zipfs_trim_path(from);

  if (zip_entry_open(zip, tpath) < 0) {
    fprintf(stderr, "Unable to open entry: %s\n", tpath);
    res = -ENOENT;
    goto on_clean;
  }

  tmpfile = zipfs_create_tmpfile();

  if (zip_entry_fread(zip, tmpfile) != 0) {
    fprintf(stderr, "Unable to zip_entry_fread: %s [%s]\n", tpath, tmpfile);
    res = -ENOENT;
    goto on_clean;
  }

  free(tpath);
  zip_entry_close(zip);
  zip_close(zip);

  zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'a');

  if (!zip) {
    fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
    res = -ENOENT;
    goto on_clean;
  }

  tpath = zipfs_trim_path(to);

  if (zip_entry_open(zip, tpath) < 0) {
    fprintf(stderr, "Unable to open entry: %s\n", tpath);
    res = -ENOENT;
    goto on_clean;
  }

  if (zip_entry_fwrite(zip, tmpfile) != 0) {
    fprintf(stderr, "Unable to zip_entry_fwrite: %s [%s]\n", tpath, tmpfile);
    res = -ENOENT;
    goto on_clean;
  }

  zip_entry_close(zip);
  zip_close(zip);
  zipfs_unlink_multi(from);

on_clean:
  if (tmpfile) {
    unlink(tmpfile);
    free(tmpfile);
  }
  if (tpath)
    free(tpath);
  pthread_mutex_unlock(&zipfs.lock);
  return res;
}

static int zipfs_truncate(const char *path, off_t size,
    struct fuse_file_info *fi) {
  (void) path;
  (void) size;
  (void) fi;
  return 0;
}

static int zipfs_create(const char *path, mode_t mode,
    struct fuse_file_info *fi) {
  (void) path;
  (void) mode;
  (void) fi;
  struct file_handle *cfi;
  struct zip_t *zip;
  char *tpath;
  int res = 0;
  fi->fh = 0;

  if(strlen(path) == 1 && strcmp(path, "/") != 0)
    return -ENOENT;

  pthread_mutex_lock(&zipfs.lock);
  zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'a');

  if (!zip) {
    fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
    res = -ENOENT;
    goto on_clean;
  }

  tpath = zipfs_trim_path(path);

  if (zip_entry_open(zip, tpath) < 0) {
    fprintf(stderr, "Unable to open entry: %s\n", tpath);
    res = -ENOENT;
    free(tpath);
    goto on_clean;
  }

  free(tpath);
  zip_entry_close(zip);
  zip_close(zip);

  if ((fi->flags > 0x20000 && fi->flags < 0x20800) || fi->flags == 0x280c1) {
    cfi = malloc(sizeof(*cfi));
    cfi->tmpfilename = zipfs_create_tmpfile();
    cfi->fd = open(cfi->tmpfilename, O_WRONLY | O_CREAT | O_TRUNC, 0600);
    fi->fh = (intptr_t)cfi;
  }

  if (fi->flags == 0x20841 || fi->flags == 0x20801) {
    zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'a');

    if (!zip) {
      fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
      res = -ENOENT;
      goto on_clean;
    }

    tpath = zipfs_trim_path(path);

    if (zip_entry_open(zip, tpath) < 0) {
      fprintf(stderr, "Unable to open entry: %s\n", tpath);
      res = -ENOENT;
      free(tpath);
      goto on_clean;
    }

    free(tpath);
    zip_entry_close(zip);
    zip_close(zip);
  }

on_clean:
  zipfs_entries_reload(1);
  pthread_mutex_unlock(&zipfs.lock);
  return res;
}

static int zipfs_mkdir(const char *path, mode_t mode) {
  (void) mode;
  int res = 0;
  int tplen;
  struct zip_t *zip;
  char *tpath;

  pthread_mutex_lock(&zipfs.lock);
  zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'a');

  if (!zip) {
    fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
    res = -ENOENT;
    goto on_clean;
  }

  tpath = zipfs_trim_path(path);
  tplen = strlen(tpath);
  tpath = realloc(tpath, tplen + 2);
  tpath[tplen] = '/';
  tpath[tplen + 1] = 0;

  if (zip_entry_open(zip, tpath) < 0) {
    fprintf(stderr, "Unable to open entry: %s\n", tpath);
    res = -ENOENT;
    free(tpath);
    goto on_clean;
  }

  free(tpath);
  zip_entry_close(zip);
  zip_close(zip);

on_clean:
  zipfs_entries_reload(1);
  pthread_mutex_unlock(&zipfs.lock);
  return res;
}

static int zipfs_rmdir(const char *path) {
  struct zip_t *zip;
  char *delfiles[2];
  char *dirpath;
  int path_len = strlen(path);
  char *tpath = zipfs_trim_path(path);
  int res = 0;

  pthread_mutex_lock(&zipfs.lock);

  dirpath = malloc((path_len + 1) * sizeof(char));
  sprintf(dirpath, "%s/", tpath);
  delfiles[0] = dirpath;

  zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'd');

  if (!zip) {
    fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
    res = -ENOENT;
    goto on_clean;
  }

  if (zip_entries_delete(zip, delfiles, 1) < 0) {
    fprintf(stderr, "Unable to open entry: %s\n", path);
    res = -ENOENT;
    goto on_clean;
  }

on_clean:
  zip_close(zip);
  free(dirpath);
  free(tpath);
  zipfs_entries_reload(1);
  pthread_mutex_unlock(&zipfs.lock);
  return res;
}

static int zipfs_flush(const char *path, struct fuse_file_info *fi) {
  (void) path;
  (void) fi;
  pthread_mutex_lock(&zipfs.lock);
  zipfs_entries_reload(1);
  pthread_mutex_unlock(&zipfs.lock);
  return 0;
}

static int zipfs_chmod(const char *path, mode_t mode,
    struct fuse_file_info *fi) {
  (void) path;
  (void) mode;
  (void) fi;
  return 0;
}
static int zipfs_chown(const char *path, uid_t uid, gid_t gid,
    struct fuse_file_info *fi) {
  (void) path;
  (void) uid;
  (void) gid;
  (void) fi;
  return 0;
}

static struct fuse_operations zipfs_oper = {
  .init       = zipfs_init,
  .getattr    = zipfs_getattr,
  .readdir    = zipfs_readdir,
  .open       = zipfs_open,
  .read       = zipfs_read,
  .write      = zipfs_write,
  .truncate   = zipfs_truncate,
  .create     = zipfs_create,
  .unlink     = zipfs_unlink,
  .chmod      = zipfs_chmod,
  .chown      = zipfs_chown,
  .release    = zipfs_release,
  .flush      = zipfs_flush,
  .rename     = zipfs_rename,
  .mkdir      = zipfs_mkdir,
  .rmdir      = zipfs_rmdir,
};

static int zipfs_opt_proc(
    void* data, const char* arg, int key, struct fuse_args* outargs) {

  (void) data;
  (void) outargs;
  char relbuf[BUFSIZ];

  switch (key) {
    case FUSE_OPT_KEY_OPT:
      return 1;
    case FUSE_OPT_KEY_NONOPT:
      if (!zipfs.zipfile) {
        realpath(arg, relbuf);
        zipfs.zipfile = strdup(relbuf);
        return 0;
      } else {
        zipfs.mountpoint = strdup(arg);
        return 1;
      }
    case KEY_HELP:
      usage(outargs->argv[0]);
      fuse_opt_add_arg(outargs, "-ho");
      exit(1);
    case KEY_VERBOSE:
      zipfs.verbose = 1;
      return 0;
    case KEY_VERSION:
      fprintf(stderr, "fusezipfs3 %s fuse/%u.%u\n",
          VERSION,
          FUSE_MAJOR_VERSION,
          FUSE_MINOR_VERSION);
      exit(1);
    default:
      exit(1);
  }
}

static void usage(const char* progname) {
  fprintf(stderr,
      "usage: %s <zipfile> <mountpoint>\n"
      "\n"
      "fusezipFS3 options:\n"
      "    -o tmpdir=<TMPDIR>     default is: /tmp\n"
      "    -o opt,[opt...]        zipfs options\n"
      "    -h   --help            print help\n"
      "    -V   --version         print version\n"
      "\n", progname);
}

#define zipFS_OPT(t, p, v) { t, offsetof(struct zipfs, p), v }

static struct fuse_opt zipfs_opts[] = {
  zipFS_OPT("zipfs_debug=%u",     debug, 0),
  zipFS_OPT("zipfs_tmpdir=%s",    tmpdir, 0),

  FUSE_OPT_KEY("-h",             KEY_HELP),
  FUSE_OPT_KEY("--help",         KEY_HELP),
  FUSE_OPT_KEY("-v",             KEY_VERBOSE),
  FUSE_OPT_KEY("--verbose",      KEY_VERBOSE),
  FUSE_OPT_KEY("-V",             KEY_VERSION),
  FUSE_OPT_KEY("--version",      KEY_VERSION),
  FUSE_OPT_END
};

int main(int argc, char** argv) {
  struct zip_t *zip;
  struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
  char *tmp;

  memset(&zipfs, 0, sizeof(zipfs));

  if (fuse_opt_parse(&args, &zipfs, zipfs_opts, zipfs_opt_proc) == -1) {
    fprintf(stderr, "fuse_opt_parse() error\n");
    exit(1);
  }

  if (!zipfs.zipfile) {
    fprintf(stderr, "missing <zipfile>\n");
    fprintf(stderr, "see `%s -h' for usage\n", argv[0]);
    exit(1);
  }

  if (!zipfs.mountpoint) {
    fprintf(stderr, "missing <mountpoint>\n");
    fprintf(stderr, "see `%s -h' for usage\n", argv[0]);
    exit(1);
  }

  if (!zipfs.tmpdir)
    zipfs.tmpdir = "/tmp";

  /*
   * Check if zip file exists
   * otherwise create it
   */
  zip = zip_open(zipfs.zipfile, 0, 'r');

  if (!zip) {
    zip_close(zip);
    zip = zip_open(zipfs.zipfile, ZIP_DEFAULT_COMPRESSION_LEVEL, 'w');
    if (!zip) {
      fprintf(stderr, "Unable to open: %s\n", zipfs.zipfile);
      exit(2);
    }
  }

  zip_close(zip);

  pthread_mutex_init(&zipfs.lock, NULL);

  // Set the filesystem name to show the current server
  tmp = malloc(BUFSIZ * sizeof(char));
  snprintf(tmp, BUFSIZ, "-ofsname=fusezipfs3#%s", zipfs.zipfile);
  fuse_opt_insert_arg(&args, 1, tmp);
  free(tmp);

  /*  fuse_main(args.argc, args.argv, cache_wrap(&zipfs_oper), NULL);*/
  fuse_main(args.argc, args.argv, &zipfs_oper, NULL);
  fuse_opt_free_args(&args);

  return 0;
}

